經過了一週的時間,與前幾篇文章的介紹,對於 Web Component 應該已經有了初步的理解了。
我們從如何透過 template
與 Shadow DOM
來封裝內容,到監聽屬性變更並觸發元件更新,自訂元件也正一步一步的從靜態邁向動態互動
中。
那麼下一步,我們要思考:元件該如何和外部世界互動?
元件要怎麼把內部發生的事情告訴外部?事件要怎麼傳遞出去?
身為一個前端工程師,對於click
, input
, change
等等事件一定再熟悉不過了。
但也別忘了,在前面我們有提到過 Shadow DOM,它就像是一個密閉的房間,所以裡面的事件不一定能直接傳到外面。
這裡有點像在 Angular,用 @Output
+ EventEmitter
把事件傳出去。
在 React,透過父元件的 callback
,子元件在事件發生時呼叫 callback,把事件往上送。
而在 Web Component,就需要用到 CustomEvent
將你的事件傳遞出去。
在我們將事件傳遞出去時,我們必須要先建立監聽觸發事件的對象,如果 listener 還沒綁定,事件就無法被接收到。
我相信 addEventListener
大家應該都很熟悉了!
element.addEventListener('click', () => {});
dispatchEvent
產生要傳遞出去的事件物件 dispatchEvent(new CustomEvent('自訂事件名稱', { 事件設定 }));
-
做分隔。event.detail
讀取。false
,若設成 true
,則事件可以一路往上冒泡,讓父層節點也能接到。false
,若設成 true
,則事件可以穿越 Shadow DOM 邊界,傳到元件外部,如果不設定,事件就只會在 Shadow Dom 內部。完整綁定寫法:
bindCustomEvent() {
const bindEvent = () => {
this.dispatchEvent(new CustomEvent('spinner-update', {
detail: { progress: 50 }, // 事件攜帶的額外資訊
bubbles: true, // 是否冒泡到外層 DOM
composed: true // 是否可以穿越 shadow DOM
}));
};
element.addEventListener('click', bindEvent);
}
在 Shadow DOM 中傳遞事件會需要注意以下兩點:
<cat-spinner>
(外部看不到你內部的真實節點)。如果要看完整路徑,需要用 event.composedPath()。this.attachShadow({ mode: 'open' })
。composedPath() 此 API 的方法會傳回 Event 事件的路徑,該路徑是一個 Array,其中包含將在其上呼叫監聽器的物件。但是如果影子根是在 ShadowRoot.mode 關閉的情況下建立的,則此方法不包含影子樹中的節點。 - MDN
composed: true
的事件,才會從 shadow tree 冒泡到外層。我們將 cat-spinner
來做延伸,這次替它加上一個 overlay(遮罩)。
當使用者點擊 overlay 時,spinner 會透過 CustomEvent 通知外部 overlay 被點擊了。
button.js
renderTemplate = function(){
const spinnerTemplate = document.createElement('template');
spinnerTemplate.innerHTML = `
<style>
.overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
display: flex;
justify-content: center;
align-items: center;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e8e5e5;
border-top-color: #926dec;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
<div class="overlay">
<div class="spinner"></div>
</div>
`;
return spinnerTemplate;
}
class CatSpinner extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
const cloneNode = this.renderTemplate().content.cloneNode(true);
shadowRoot.appendChild(cloneNode);
}
// 元件被加入 DOM 時綁定事件
connectedCallback() {
this.bindOverlayClickEvent();
}
bindOverlayClickEvent() {
const overlay = this.shadowRoot.querySelector('.overlay');
const bindEvent = ()=> {
// 加入自訂事件
this.dispatchEvent(new CustomEvent('overlay-click', {
detail: { overlayClick: true }
bubbles: true,
composed: true
}));
}
overlay.addEventListener('click', bindEvent); // 綁定要傳遞出去的事件
}
}
customElements.define('cat-spinner', CatSpinner);
event.detail
取得事件的附加資訊。index.html
<body>
<cat-spinner></cat-spinner>
<script src="spinner.js"></script>
<script>
const spinner = document.querySelector('cat-spinner');
spinner.addEventListener('overlay-click', (event) => {
console.log('使用者點擊了 overlay!');
console.log('事件 detail:', event.detail); // 使用 event.detail 取出 detail 內容
});
</script>
</body>
cat-spinner
在外部監聽綁定的事件,加入移除 cat-spinner
的邏輯,當 <cat-spinner>
從 DOM 被移除時,它的 shadowRoot 也會一起被銷毀。瀏覽器會連同內部的 DOM、事件監聽器一併清理掉,所以不會造成記憶體洩漏。
index.html
<body>
<cat-spinner></cat-spinner>
<script src="spinner.js"></script>
<script>
const spinner = document.querySelector('cat-spinner');
spinner.addEventListener('overlay-click', (event) => {
if (event.detail.overlayClick) {
spinner.remove();
}
});
</script>
</body>